diff options
Diffstat (limited to 'app/[lng]')
61 files changed, 3007 insertions, 0 deletions
diff --git a/app/[lng]/evcp/bqtbe/page.tsx b/app/[lng]/evcp/bqtbe/page.tsx new file mode 100644 index 00000000..655bd30a --- /dev/null +++ b/app/[lng]/evcp/bqtbe/page.tsx @@ -0,0 +1,72 @@ +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { getAllTBE } from "@/lib/rfqs/service" +import { searchParamsTBECache } from "@/lib/rfqs/validations" +import { AllTbeTable } from "@/lib/tbe/table/tbe-table" +import { RfqType } from "@/lib/rfqs/validations" +import * as React from "react" +import { Shell } from "@/components/shell" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" + +interface IndexPageProps { + // Next.js 13 App Router에서 기본으로 주어지는 객체들 + params: { + lng: string + } + searchParams: Promise<SearchParams> + rfqType: RfqType +} + +export default async function RfqTBEPage(props: IndexPageProps) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + + const rfqType = props.rfqType ?? RfqType.BUDGETARY // rfqType이 없으면 BUDGETARY로 설정 + + // 2) SearchParams 파싱 (Zod) + // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 + const searchParams = await props.searchParams + const search = searchParamsTBECache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getAllTBE({ + ...search, + filters: validFilters, + rfqType + } + ) + ]) + + // 4) 렌더링 + return ( + <Shell className="gap-2"> + <div className="flex items-center justify-between space-y-2"> + <div className="flex items-center justify-between space-y-2"> + <div> + <h2 className="text-2xl font-bold tracking-tight"> + Technical Bid Evaluation + </h2> + <p className="text-muted-foreground"> + 초대된 벤더에게 TBE를 보낼 수 있습니다. <br/>체크박스 선택을 하면 초대 버튼이 활성화됩니다. 버튼 클릭 후 첨부파일을 함께 전송하면 TBE 내용이 메일로 전달되고 eVCP에도 벤더가 입력할 수 있게 자동 생성됩니다. + </p> + </div> + </div> + </div> + + <React.Suspense + fallback={ + <DataTableSkeleton + columnCount={6} + searchableColumnCount={1} + filterableColumnCount={2} + cellWidths={["10rem", "40rem", "12rem", "12rem", "8rem", "8rem"]} + shrinkZero + /> + } + > + <AllTbeTable promises={promises}/> + </React.Suspense> + </Shell> + ) +}
\ No newline at end of file diff --git a/app/[lng]/evcp/budgetary/[id]/cbe/page.tsx b/app/[lng]/evcp/budgetary/[id]/cbe/page.tsx new file mode 100644 index 00000000..9a4ae7eb --- /dev/null +++ b/app/[lng]/evcp/budgetary/[id]/cbe/page.tsx @@ -0,0 +1,56 @@ +import { Separator } from "@/components/ui/separator" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { getCBE, getTBE } from "@/lib/rfqs/service" +import { searchParamsCBECache, } from "@/lib/rfqs/validations" +import { TbeTable } from "@/lib/rfqs/tbe-table/tbe-table" +import { CbeTable } from "@/lib/rfqs/cbe-table/cbe-table" + +interface IndexPageProps { + // Next.js 13 App Router에서 기본으로 주어지는 객체들 + params: { + lng: string + id: string + } + searchParams: Promise<SearchParams> +} + +export default async function RfqTBEPage(props: IndexPageProps) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + const id = resolvedParams.id + + const idAsNumber = Number(id) + + // 2) SearchParams 파싱 (Zod) + // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 + const searchParams = await props.searchParams + const search = searchParamsCBECache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getCBE({ + ...search, + filters: validFilters, + }, + idAsNumber) + ]) + + // 4) 렌더링 + return ( + <div className="space-y-6"> + <div> + <h3 className="text-lg font-medium"> + Commercial Bid Evaluation + </h3> + <p className="text-sm text-muted-foreground"> + 초대된 벤더에게 CBE를 보낼 수 있습니다. <br/>체크박스 선택을 하면 초대 버튼이 활성화됩니다. 버튼 클릭 후 첨부파일을 함께 전송하면 TBE 내용이 메일로 전달되고 eVCP에도 벤더가 입력할 수 있게 자동 생성됩니다. + </p> + </div> + <Separator /> + <div> + <CbeTable promises={promises} rfqId={idAsNumber}/> + </div> + </div> + ) +}
\ No newline at end of file diff --git a/app/[lng]/evcp/budgetary/[id]/layout.tsx b/app/[lng]/evcp/budgetary/[id]/layout.tsx new file mode 100644 index 00000000..39f045e5 --- /dev/null +++ b/app/[lng]/evcp/budgetary/[id]/layout.tsx @@ -0,0 +1,80 @@ +import { Metadata } from "next" + +import { Separator } from "@/components/ui/separator" +import { SidebarNav } from "@/components/layout/sidebar-nav" +import { findVendorById } from "@/lib/vendors/service" // 가정: 여기에 findVendorById가 있다고 가정 +import { Rfq, RfqWithItems } from "@/db/schema/rfq" +import { findRfqById } from "@/lib/rfqs/service" +import { formatDate } from "@/lib/utils" + +export const metadata: Metadata = { + title: "Vendor Detail", +} + +export default async function RfqLayout({ + children, + params, +}: { + children: React.ReactNode + params: { lng: string, id: string } +}) { + + // 1) URL 파라미터에서 id 추출, Number로 변환 + const resolvedParams = await params + const lng = resolvedParams.lng + const id = resolvedParams.id + + const idAsNumber = Number(id) + // 2) DB에서 해당 벤더 정보 조회 + const rfq: RfqWithItems | null = await findRfqById(idAsNumber) + + // 3) 사이드바 메뉴 + const sidebarNavItems = [ + { + title: "Matched Vendors", + href: `/${lng}/evcp/budgetary/${id}`, + }, + { + title: "TBE", + href: `/${lng}/evcp/budgetary/${id}/tbe`, + }, + { + title: "CBE", + href: `/${lng}/evcp/budgetary/${id}/cbe`, + }, + + ] + + return ( + <> + <div className="container py-6"> + <section className="overflow-hidden rounded-[0.5rem] border bg-background shadow"> + <div className="hidden space-y-6 p-10 pb-16 md:block"> + <div className="space-y-0.5"> + {/* 4) 벤더 정보가 있으면 코드 + 이름 + "상세 정보" 표기 */} + <h2 className="text-2xl font-bold tracking-tight"> + {rfq + ? `${rfq.rfqCode ?? ""} 관리` + : "Loading RFQ..."} + </h2> + + <p className="text-muted-foreground"> + {rfq + ? `${rfq.description ?? ""} ${rfq.lines.map(line => line.itemCode).join(", ")}` + : ""} + </p> + <h3>Due Date:{ rfq && <strong>{formatDate(rfq?.dueDate)}</strong>}</h3> + </div> + <Separator className="my-6" /> + <div className="flex flex-col space-y-8 lg:flex-row lg:space-x-12 lg:space-y-0"> + <aside className="-mx-4 lg:w-1/6"> + <SidebarNav items={sidebarNavItems} /> + </aside> + <div className="flex-1">{children}</div> + </div> + </div> + </section> + </div> + </> + ) +}
\ No newline at end of file diff --git a/app/[lng]/evcp/budgetary/[id]/page.tsx b/app/[lng]/evcp/budgetary/[id]/page.tsx new file mode 100644 index 00000000..f6160574 --- /dev/null +++ b/app/[lng]/evcp/budgetary/[id]/page.tsx @@ -0,0 +1,57 @@ +import { Separator } from "@/components/ui/separator" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { getMatchedVendors } from "@/lib/rfqs/service" +import { searchParamsMatchedVCache } from "@/lib/rfqs/validations" +import { MatchedVendorsTable } from "@/lib/rfqs/vendor-table/vendors-table" +import { RfqType } from "@/lib/rfqs/validations" + +interface IndexPageProps { + // Next.js 13 App Router에서 기본으로 주어지는 객체들 + params: { + lng: string + id: string + } + searchParams: Promise<SearchParams> + rfqType: RfqType +} + +export default async function RfqPage(props: IndexPageProps) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + const id = resolvedParams.id + const rfqType = props.rfqType ?? RfqType.BUDGETARY // rfqType이 없으면 BUDGETARY로 설정 + + const idAsNumber = Number(id) + + // 2) SearchParams 파싱 (Zod) + const searchParams = await props.searchParams + const search = searchParamsMatchedVCache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getMatchedVendors({ + ...search, + filters: validFilters, + }, + idAsNumber) + ]) + + // 4) 렌더링 + return ( + <div className="space-y-6"> + <div> + <h3 className="text-lg font-medium"> + Vendors + </h3> + <p className="text-sm text-muted-foreground"> + 등록된 벤더 중에서 이 RFQ 아이템에 매칭되는 업체를 보여줍니다. <br/>"발행하기" 버튼을 통해 RFQ를 전송하면 첨부파일과 함께 RFQ 내용이 메일로 전달되고 eVCP에도 벤더가 입력할 수 있게 자동 생성됩니다. + </p> + </div> + <Separator /> + <div> + <MatchedVendorsTable promises={promises} rfqId={idAsNumber} rfqType={rfqType}/> + </div> + </div> + ) +}
\ No newline at end of file diff --git a/app/[lng]/evcp/budgetary/[id]/tbe/page.tsx b/app/[lng]/evcp/budgetary/[id]/tbe/page.tsx new file mode 100644 index 00000000..a6259696 --- /dev/null +++ b/app/[lng]/evcp/budgetary/[id]/tbe/page.tsx @@ -0,0 +1,55 @@ +import { Separator } from "@/components/ui/separator" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { getTBE } from "@/lib/rfqs/service" +import { searchParamsTBECache } from "@/lib/rfqs/validations" +import { TbeTable } from "@/lib/rfqs/tbe-table/tbe-table" + +interface IndexPageProps { + // Next.js 13 App Router에서 기본으로 주어지는 객체들 + params: { + lng: string + id: string + } + searchParams: Promise<SearchParams> +} + +export default async function RfqTBEPage(props: IndexPageProps) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + const id = resolvedParams.id + + const idAsNumber = Number(id) + + // 2) SearchParams 파싱 (Zod) + // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 + const searchParams = await props.searchParams + const search = searchParamsTBECache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getTBE({ + ...search, + filters: validFilters, + }, + idAsNumber) + ]) + + // 4) 렌더링 + return ( + <div className="space-y-6"> + <div> + <h3 className="text-lg font-medium"> + Technical Bid Evaluation + </h3> + <p className="text-sm text-muted-foreground"> + 초대된 벤더에게 TBE를 보낼 수 있습니다. <br/>체크박스 선택을 하면 초대 버튼이 활성화됩니다. 버튼 클릭 후 첨부파일을 함께 전송하면 TBE 내용이 메일로 전달되고 eVCP에도 벤더가 입력할 수 있게 자동 생성됩니다. + </p> + </div> + <Separator /> + <div> + <TbeTable promises={promises} rfqId={idAsNumber}/> + </div> + </div> + ) +}
\ No newline at end of file diff --git a/app/[lng]/evcp/budgetary/page.tsx b/app/[lng]/evcp/budgetary/page.tsx new file mode 100644 index 00000000..04550353 --- /dev/null +++ b/app/[lng]/evcp/budgetary/page.tsx @@ -0,0 +1,86 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" + +import { searchParamsCache } from "@/lib/rfqs/validations" +import { getRfqs, getRfqStatusCounts } from "@/lib/rfqs/service" +import { RfqsTable } from "@/lib/rfqs/table/rfqs-table" +import { getAllItems } from "@/lib/items/service" +import { RfqType } from "@/lib/rfqs/validations" +import { Ellipsis } from "lucide-react" + +interface RfqPageProps { + searchParams: Promise<SearchParams>; + rfqType: RfqType; + title: string; + description: string; +} + +export default async function RfqPage({ + searchParams, + rfqType = RfqType.BUDGETARY, + title = "Budgetary Quote", + description = "Budgetary Quote를 등록하여 요청 및 응답을 관리할 수 있습니다." +}: RfqPageProps) { + const search = searchParamsCache.parse(await searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getRfqs({ + ...search, + filters: validFilters, + rfqType // 전달받은 rfqType 사용 + }), + getRfqStatusCounts(rfqType), // rfqType 전달 + getAllItems() + ]) + + return ( + <Shell className="gap-2"> + <div className="flex items-center justify-between space-y-2"> + <div className="flex items-center justify-between space-y-2"> + <div> + <h2 className="text-2xl font-bold tracking-tight"> + {title} + </h2> + <p className="text-muted-foreground"> + {description} + 기본적인 정보와 RFQ를 위한 아이템 등록 및 첨부를 한 후, + <span className="inline-flex items-center whitespace-nowrap"> + <Ellipsis className="size-3" /> + <span className="ml-1">버튼</span> + </span> 을 클릭하면 "Proceed"를 통해 상세화면으로 이동하여 진행할 수 있습니다. + </p> + </div> + </div> + </div> + + <React.Suspense fallback={<Skeleton className="h-7 w-52" />}> + {/* <DateRangePicker + triggerSize="sm" + triggerClassName="ml-auto w-56 sm:w-60" + align="end" + shallow={false} + /> */} + </React.Suspense> + <React.Suspense + fallback={ + <DataTableSkeleton + columnCount={6} + searchableColumnCount={1} + filterableColumnCount={2} + cellWidths={["10rem", "40rem", "12rem", "12rem", "8rem", "8rem"]} + shrinkZero + /> + } + > + <RfqsTable promises={promises} rfqType={rfqType} /> + </React.Suspense> + </Shell> + ) +}
\ No newline at end of file diff --git a/app/[lng]/evcp/equip-class/page.tsx b/app/[lng]/evcp/equip-class/page.tsx new file mode 100644 index 00000000..fcda1c1d --- /dev/null +++ b/app/[lng]/evcp/equip-class/page.tsx @@ -0,0 +1,75 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" +import { searchParamsCache } from "@/lib/equip-class/validation" +import { FormListsTable } from "@/lib/form-list/table/formLists-table" +import { getTagClassists } from "@/lib/equip-class/service" +import { EquipClassTable } from "@/lib/equip-class/table/equipClass-table" + + +interface IndexPageProps { + searchParams: Promise<SearchParams> +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = searchParamsCache.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getTagClassists({ + ...search, + filters: validFilters, + }), + + ]) + + return ( + <Shell className="gap-2"> + <div className="flex items-center justify-between space-y-2"> + <div className="flex items-center justify-between space-y-2"> + <div> + <h2 className="text-2xl font-bold tracking-tight"> + Object Class List from S-EDP + </h2> + <p className="text-muted-foreground"> + 벤더 데이터 입력을 위한 Form 리스트입니다.{" "} + {/* <span className="inline-flex items-center whitespace-nowrap"> + <Ellipsis className="size-3" /> + <span className="ml-1">버튼</span> + </span> + 을 통해 담당자 연락처, 입찰 이력, 계약 이력, 패키지 내용 등을 확인 할 수 있습니다. */} + </p> + </div> + </div> + </div> + + <React.Suspense fallback={<Skeleton className="h-7 w-52" />}> + {/* <DateRangePicker + triggerSize="sm" + triggerClassName="ml-auto w-56 sm:w-60" + align="end" + shallow={false} + /> */} + </React.Suspense> + <React.Suspense + fallback={ + <DataTableSkeleton + columnCount={6} + searchableColumnCount={1} + filterableColumnCount={2} + cellWidths={["10rem", "40rem", "12rem", "12rem", "8rem", "8rem"]} + shrinkZero + /> + } + > + <EquipClassTable promises={promises} /> + </React.Suspense> + </Shell> + ) +} diff --git a/app/[lng]/evcp/faq/manage/actions.ts b/app/[lng]/evcp/faq/manage/actions.ts new file mode 100644 index 00000000..bc443a8a --- /dev/null +++ b/app/[lng]/evcp/faq/manage/actions.ts @@ -0,0 +1,48 @@ +'use server';
+
+import { promises as fs } from 'fs';
+import path from 'path';
+import { FaqCategory } from '@/components/faq/FaqCard';
+import { fallbackLng } from '@/i18n/settings';
+
+const FAQ_CONFIG_PATH = path.join(process.cwd(), 'config', 'faqDataConfig.ts');
+
+export async function updateFaqData(lng: string, newData: FaqCategory[]) {
+ try {
+ const fileContent = await fs.readFile(FAQ_CONFIG_PATH, 'utf-8');
+ const dataMatch = fileContent.match(/export const faqCategories[^=]*=\s*(\{[\s\S]*\});/);
+ if (!dataMatch) {
+ throw new Error('FAQ 데이터 형식이 올바르지 않습니다.');
+ }
+
+ const allData = eval(`(${dataMatch[1]})`);
+ const updatedData = {
+ ...allData,
+ [lng]: newData
+ };
+
+ const newFileContent = `import { FaqCategory } from "@/components/faq/FaqCard";\n\ninterface LocalizedFaqCategories {\n [lng: string]: FaqCategory[];\n}\n\nexport const faqCategories: LocalizedFaqCategories = ${JSON.stringify(updatedData, null, 4)};`;
+ await fs.writeFile(FAQ_CONFIG_PATH, newFileContent, 'utf-8');
+
+ return { success: true };
+ } catch (error) {
+ console.error('FAQ 데이터 업데이트 중 오류 발생:', error);
+ return { success: false, error: '데이터 업데이트 중 오류가 발생했습니다.' };
+ }
+}
+
+export async function getFaqData(lng: string): Promise<{ data: FaqCategory[] }> {
+ try {
+ const fileContent = await fs.readFile(FAQ_CONFIG_PATH, 'utf-8');
+ const dataMatch = fileContent.match(/export const faqCategories[^=]*=\s*(\{[\s\S]*\});/);
+ if (!dataMatch) {
+ throw new Error('FAQ 데이터 형식이 올바르지 않습니다.');
+ }
+
+ const allData = eval(`(${dataMatch[1]})`);
+ return { data: allData[lng] || allData[fallbackLng] || [] };
+ } catch (error) {
+ console.error('FAQ 데이터 읽기 중 오류 발생:', error);
+ return { data: [] };
+ }
+}
\ No newline at end of file diff --git a/app/[lng]/evcp/faq/manage/page.tsx b/app/[lng]/evcp/faq/manage/page.tsx new file mode 100644 index 00000000..011bbfa4 --- /dev/null +++ b/app/[lng]/evcp/faq/manage/page.tsx @@ -0,0 +1,38 @@ +import { FaqManager } from '@/components/faq/FaqManager';
+import { getFaqData, updateFaqData } from './actions';
+import { revalidatePath } from 'next/cache';
+import { FaqCategory } from '@/components/faq/FaqCard';
+
+interface Props {
+ params: {
+ lng: string;
+ }
+}
+
+export default async function FaqManagePage(props: Props) {
+ const resolvedParams = await props.params
+ const lng = resolvedParams.lng
+ const { data } = await getFaqData(lng);
+
+ async function handleSave(newData: FaqCategory[]) {
+ 'use server';
+ await updateFaqData(lng, newData);
+ revalidatePath(`/${lng}/evcp/faq`);
+ }
+
+ return (
+ <div className="container py-6">
+ <section className="overflow-hidden rounded-[0.5rem] border bg-background shadow">
+ <div className="space-y-6 p-10 pb-16">
+ <div className="space-y-0.5">
+ <h2 className="text-2xl font-bold tracking-tight">FAQ Management</h2>
+ <p className="text-muted-foreground">
+ Manage FAQ categories and items for {lng.toUpperCase()} language.
+ </p>
+ </div>
+ <FaqManager initialData={data} onSave={handleSave} lng={lng} />
+ </div>
+ </section>
+ </div>
+ );
+}
\ No newline at end of file diff --git a/app/[lng]/evcp/faq/page.tsx b/app/[lng]/evcp/faq/page.tsx new file mode 100644 index 00000000..9b62b7e4 --- /dev/null +++ b/app/[lng]/evcp/faq/page.tsx @@ -0,0 +1,62 @@ +import { Separator } from "@/components/ui/separator"
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
+import { faqCategories } from "@/config/faqDataConfig"
+import { FaqCard } from "@/components/faq/FaqCard"
+import { Button } from "@/components/ui/button"
+import { Settings } from "lucide-react"
+import Link from "next/link"
+import { fallbackLng } from "@/i18n/settings"
+
+interface Props {
+ params: {
+ lng: string;
+ }
+}
+
+export default async function FaqPage(props: Props) {
+ const resolvedParams = await props.params
+ const lng = resolvedParams.lng
+ const localizedFaqCategories = faqCategories[lng] || faqCategories[fallbackLng];
+
+ return (
+ <div className="container py-6">
+ <section className="overflow-hidden rounded-[0.5rem] border bg-background shadow">
+ <div className="space-y-6 p-10 pb-16">
+ <div className="flex justify-between items-center">
+ <div className="space-y-0.5">
+ <h2 className="text-2xl font-bold tracking-tight">Frequently Asked Questions</h2>
+ <p className="text-muted-foreground">
+ Find answers to common questions about using the EVCP system.
+ </p>
+ </div>
+ <Link href={`/${lng}/evcp/faq/manage`}>
+ <Button variant="outline">
+ <Settings className="w-4 h-4 mr-2" />
+ Manage FAQ
+ </Button>
+ </Link>
+ </div>
+ <Separator className="my-6" />
+
+ <Tabs defaultValue={localizedFaqCategories[0]?.label} className="space-y-4">
+ <TabsList>
+ {localizedFaqCategories.map((category) => (
+ <TabsTrigger key={category.label} value={category.label}>
+ {category.label}
+ </TabsTrigger>
+ ))}
+ </TabsList>
+
+ {localizedFaqCategories.map((category) => (
+ <TabsContent key={category.label} value={category.label} className="space-y-4">
+ {category.items.map((item, index) => (
+ <FaqCard key={index} item={item} />
+ ))}
+ </TabsContent>
+ ))}
+ </Tabs>
+ </div>
+ </section>
+ </div>
+ )
+}
diff --git a/app/[lng]/evcp/form-list/page.tsx b/app/[lng]/evcp/form-list/page.tsx new file mode 100644 index 00000000..f96917d6 --- /dev/null +++ b/app/[lng]/evcp/form-list/page.tsx @@ -0,0 +1,75 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" +import { searchParamsCache } from "@/lib/form-list/validation" +import { ItemsTable } from "@/lib/items/table/items-table" +import { getFormLists } from "@/lib/form-list/service" +import { FormListsTable } from "@/lib/form-list/table/formLists-table" + + +interface IndexPageProps { + searchParams: Promise<SearchParams> +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = searchParamsCache.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getFormLists({ + ...search, + filters: validFilters, + }), + + ]) + + return ( + <Shell className="gap-2"> + <div className="flex items-center justify-between space-y-2"> + <div className="flex items-center justify-between space-y-2"> + <div> + <h2 className="text-2xl font-bold tracking-tight"> + Form List from S-EDP + </h2> + <p className="text-muted-foreground"> + 벤더 데이터 입력을 위한 Form 리스트입니다.{" "} + {/* <span className="inline-flex items-center whitespace-nowrap"> + <Ellipsis className="size-3" /> + <span className="ml-1">버튼</span> + </span> + 을 통해 담당자 연락처, 입찰 이력, 계약 이력, 패키지 내용 등을 확인 할 수 있습니다. */} + </p> + </div> + </div> + </div> + + <React.Suspense fallback={<Skeleton className="h-7 w-52" />}> + {/* <DateRangePicker + triggerSize="sm" + triggerClassName="ml-auto w-56 sm:w-60" + align="end" + shallow={false} + /> */} + </React.Suspense> + <React.Suspense + fallback={ + <DataTableSkeleton + columnCount={6} + searchableColumnCount={1} + filterableColumnCount={2} + cellWidths={["10rem", "40rem", "12rem", "12rem", "8rem", "8rem"]} + shrinkZero + /> + } + > + <FormListsTable promises={promises} /> + </React.Suspense> + </Shell> + ) +} diff --git a/app/[lng]/evcp/items/page.tsx b/app/[lng]/evcp/items/page.tsx new file mode 100644 index 00000000..144689ff --- /dev/null +++ b/app/[lng]/evcp/items/page.tsx @@ -0,0 +1,74 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" +import { searchParamsCache } from "@/lib/items/validations" +import { getItems } from "@/lib/items/service" +import { ItemsTable } from "@/lib/items/table/items-table" + + +interface IndexPageProps { + searchParams: Promise<SearchParams> +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = searchParamsCache.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getItems({ + ...search, + filters: validFilters, + }), + + ]) + + return ( + <Shell className="gap-2"> + <div className="flex items-center justify-between space-y-2"> + <div className="flex items-center justify-between space-y-2"> + <div> + <h2 className="text-2xl font-bold tracking-tight"> + Package Items + </h2> + <p className="text-muted-foreground"> + Item을 등록하고 관리할 수 있습니다.{" "} + {/* <span className="inline-flex items-center whitespace-nowrap"> + <Ellipsis className="size-3" /> + <span className="ml-1">버튼</span> + </span> + 을 통해 담당자 연락처, 입찰 이력, 계약 이력, 패키지 내용 등을 확인 할 수 있습니다. */} + </p> + </div> + </div> + </div> + + <React.Suspense fallback={<Skeleton className="h-7 w-52" />}> + {/* <DateRangePicker + triggerSize="sm" + triggerClassName="ml-auto w-56 sm:w-60" + align="end" + shallow={false} + /> */} + </React.Suspense> + <React.Suspense + fallback={ + <DataTableSkeleton + columnCount={6} + searchableColumnCount={1} + filterableColumnCount={2} + cellWidths={["10rem", "40rem", "12rem", "12rem", "8rem", "8rem"]} + shrinkZero + /> + } + > + <ItemsTable promises={promises} /> + </React.Suspense> + </Shell> + ) +} diff --git a/app/[lng]/evcp/layout.tsx b/app/[lng]/evcp/layout.tsx new file mode 100644 index 00000000..9dc39f7b --- /dev/null +++ b/app/[lng]/evcp/layout.tsx @@ -0,0 +1,17 @@ +import { ReactNode } from 'react'; +import { Header } from '@/components/layout/Header'; +import { SiteFooter } from '@/components/layout/Footer'; + +export default function EvcpLayout({ children }: { children: ReactNode }) { + return ( + <div className="relative flex min-h-svh flex-col bg-background"> + <Header /> + <main className="flex flex-1 flex-col"> + <div className='container-wrapper'> + {children} + </div> + </main> + <SiteFooter/> + </div> + ); +}
\ No newline at end of file diff --git a/app/[lng]/evcp/page.tsx b/app/[lng]/evcp/page.tsx new file mode 100644 index 00000000..a1e9f8be --- /dev/null +++ b/app/[lng]/evcp/page.tsx @@ -0,0 +1,8 @@ + +export default function Pages() { + return ( + <> + test + </> + ) + }
\ No newline at end of file diff --git a/app/[lng]/evcp/po/page.tsx b/app/[lng]/evcp/po/page.tsx new file mode 100644 index 00000000..fa528df0 --- /dev/null +++ b/app/[lng]/evcp/po/page.tsx @@ -0,0 +1,65 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" +import { getPOs } from "@/lib/po/service" +import { searchParamsCache } from "@/lib/po/validations" +import { PoListsTable } from "@/lib/po/table/po-table" + + +interface IndexPageProps { + searchParams: Promise<SearchParams> +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = searchParamsCache.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getPOs({ + ...search, + filters: validFilters, + }), + ]) + + return ( + <Shell className="gap-2"> + + <div className="flex items-center justify-between space-y-2"> + <div className="flex items-center justify-between space-y-2"> + <div> + <h2 className="text-2xl font-bold tracking-tight"> + PO 확인 및 전자서명 + </h2> + <p className="text-muted-foreground"> + 기간계 시스템으로부터 PO를 확인하고 벤더에게 전자서명을 요청할 수 있습니다. 요쳥된 전자서명의 이력 또한 확인할 수 있습니다. + + </p> + </div> + </div> + </div> + + + <React.Suspense fallback={<Skeleton className="h-7 w-52" />}> + </React.Suspense> + <React.Suspense + fallback={ + <DataTableSkeleton + columnCount={6} + searchableColumnCount={1} + filterableColumnCount={2} + cellWidths={["10rem", "40rem", "12rem", "12rem", "8rem", "8rem"]} + shrinkZero + /> + } + > + <PoListsTable promises={promises} /> + </React.Suspense> + </Shell> + ) +} diff --git a/app/[lng]/evcp/pq-criteria/page.tsx b/app/[lng]/evcp/pq-criteria/page.tsx new file mode 100644 index 00000000..d924890d --- /dev/null +++ b/app/[lng]/evcp/pq-criteria/page.tsx @@ -0,0 +1,71 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" + +import { searchParamsCache } from "@/lib/pq/validations" +import { getPQs } from "@/lib/pq/service" +import { PqsTable } from "@/lib/pq/table/pq-table" + +interface IndexPageProps { + searchParams: Promise<SearchParams> +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = searchParamsCache.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getPQs({ + ...search, + filters: validFilters, + }), + ]) + + return ( + <Shell className="gap-2"> + + <div className="flex items-center justify-between space-y-2"> + <div className="flex items-center justify-between space-y-2"> + <div> + <h2 className="text-2xl font-bold tracking-tight"> + Pre-Qualification Check Sheet + </h2> + <p className="text-muted-foreground"> + 벤더 등록을 위한, 벤더가 제출할 PQ 항목을 관리할 수 있습니다. + + </p> + </div> + </div> + </div> + + + <React.Suspense fallback={<Skeleton className="h-7 w-52" />}> + {/* <DateRangePicker + triggerSize="sm" + triggerClassName="ml-auto w-56 sm:w-60" + align="end" + shallow={false} + /> */} + </React.Suspense> + <React.Suspense + fallback={ + <DataTableSkeleton + columnCount={6} + searchableColumnCount={1} + filterableColumnCount={2} + cellWidths={["10rem", "40rem", "12rem", "12rem", "8rem", "8rem"]} + shrinkZero + /> + } + > + <PqsTable promises={promises} /> + </React.Suspense> + </Shell> + ) +} diff --git a/app/[lng]/evcp/pq/[vendorId]/page.tsx b/app/[lng]/evcp/pq/[vendorId]/page.tsx new file mode 100644 index 00000000..cb4277f1 --- /dev/null +++ b/app/[lng]/evcp/pq/[vendorId]/page.tsx @@ -0,0 +1,38 @@ +import * as React from "react" +import { Shell } from "@/components/shell" +import { Skeleton } from "@/components/ui/skeleton" + +import { type SearchParams } from "@/types/table" +import { getPQDataByVendorId } from "@/lib/pq/service" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Vendor } from "@/db/schema/vendors" +import { findVendorById } from "@/lib/vendors/service" +import VendorPQReviewPage from "@/components/pq/pq-review-detail" +import VendorPQAdminReview from "@/components/pq/pq-review-detail" + +interface IndexPageProps { + params: { + vendorId: string // Updated from 'id' to 'contractId' to match route parameter + } + searchParams: Promise<SearchParams> +} + +export default async function DocumentListPage(props: IndexPageProps) { + const resolvedParams = await props.params + const vendorId = resolvedParams.vendorId // Updated from 'id' to 'contractId' + + const idAsNumber = Number(vendorId) + + const data = await getPQDataByVendorId(idAsNumber) + + const vendor: Vendor | null = await findVendorById(idAsNumber) + + // 4) 렌더링 + return ( + <Shell className="gap-2"> + {vendor && + <VendorPQAdminReview data={data} vendor={vendor} /> + } + </Shell> + ) +} diff --git a/app/[lng]/evcp/pq/page.tsx b/app/[lng]/evcp/pq/page.tsx new file mode 100644 index 00000000..46b22b12 --- /dev/null +++ b/app/[lng]/evcp/pq/page.tsx @@ -0,0 +1,71 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" + +import { getVendorsInPQ } from "@/lib/pq/service" +import { searchParamsCache } from "@/lib/vendors/validations" +import { VendorsPQReviewTable } from "@/lib/pq/pq-review-table/vendors-table" + +interface IndexPageProps { + searchParams: Promise<SearchParams> +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = searchParamsCache.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getVendorsInPQ({ + ...search, + filters: validFilters, + }), + ]) + + return ( + <Shell className="gap-2"> + + <div className="flex items-center justify-between space-y-2"> + <div className="flex items-center justify-between space-y-2"> + <div> + <h2 className="text-2xl font-bold tracking-tight"> + Pre-Qualification Review + </h2> + <p className="text-muted-foreground"> + 벤더가 제출한 PQ를 확인하고 수정 요청 등을 할 수 있으며 PQ 종료 후에는 통과 여부를 결정할 수 있습니다. + + </p> + </div> + </div> + </div> + + + <React.Suspense fallback={<Skeleton className="h-7 w-52" />}> + {/* <DateRangePicker + triggerSize="sm" + triggerClassName="ml-auto w-56 sm:w-60" + align="end" + shallow={false} + /> */} + </React.Suspense> + <React.Suspense + fallback={ + <DataTableSkeleton + columnCount={6} + searchableColumnCount={1} + filterableColumnCount={2} + cellWidths={["10rem", "40rem", "12rem", "12rem", "8rem", "8rem"]} + shrinkZero + /> + } + > + <VendorsPQReviewTable promises={promises} /> + </React.Suspense> + </Shell> + ) +} diff --git a/app/[lng]/evcp/report/page.tsx b/app/[lng]/evcp/report/page.tsx new file mode 100644 index 00000000..a1e9f8be --- /dev/null +++ b/app/[lng]/evcp/report/page.tsx @@ -0,0 +1,8 @@ + +export default function Pages() { + return ( + <> + test + </> + ) + }
\ No newline at end of file diff --git a/app/[lng]/evcp/rfq/[id]/cbe/page.tsx b/app/[lng]/evcp/rfq/[id]/cbe/page.tsx new file mode 100644 index 00000000..bc32641f --- /dev/null +++ b/app/[lng]/evcp/rfq/[id]/cbe/page.tsx @@ -0,0 +1,53 @@ +import { Separator } from "@/components/ui/separator" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { searchParamsTBECache } from "@/lib/rfqs/validations" + +interface IndexPageProps { + // Next.js 13 App Router에서 기본으로 주어지는 객체들 + params: { + lng: string + id: string + } + searchParams: Promise<SearchParams> +} + +export default async function RfqCBEPage(props: IndexPageProps) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + const id = resolvedParams.id + + const idAsNumber = Number(id) + + // 2) SearchParams 파싱 (Zod) + // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 + const searchParams = await props.searchParams + const search = searchParamsTBECache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + // const promises = Promise.all([ + // getCBE({ + // ...search, + // filters: validFilters, + // }, + // idAsNumber) + // ]) + + // 4) 렌더링 + return ( + <div className="space-y-6"> + <div> + <h3 className="text-lg font-medium"> + Technical Bid Evaluation + </h3> + <p className="text-sm text-muted-foreground"> + 초대된 벤더에게 CBE를 보낼 수 있습니다. <br/>"발행하기" 버튼을 통해 CBE를 전송하면 CBE 내용이 메일로 전달되고 eVCP에도 벤더가 입력할 수 있게 자동 생성됩니다. + </p> + </div> + <Separator /> + <div> + + </div> + </div> + ) +}
\ No newline at end of file diff --git a/app/[lng]/evcp/rfq/[id]/layout.tsx b/app/[lng]/evcp/rfq/[id]/layout.tsx new file mode 100644 index 00000000..2aac90eb --- /dev/null +++ b/app/[lng]/evcp/rfq/[id]/layout.tsx @@ -0,0 +1,80 @@ +import { Metadata } from "next" + +import { Separator } from "@/components/ui/separator" +import { SidebarNav } from "@/components/layout/sidebar-nav" +import { findVendorById } from "@/lib/vendors/service" // 가정: 여기에 findVendorById가 있다고 가정 +import { Rfq, RfqWithItems } from "@/db/schema/rfq" +import { findRfqById } from "@/lib/rfqs/service" +import { formatDate } from "@/lib/utils" + +export const metadata: Metadata = { + title: "Vendor Detail", +} + +export default async function RfqLayout({ + children, + params, +}: { + children: React.ReactNode + params: { lng: string, id: string } +}) { + + // 1) URL 파라미터에서 id 추출, Number로 변환 + const resolvedParams = await params + const lng = resolvedParams.lng + const id = resolvedParams.id + + const idAsNumber = Number(id) + // 2) DB에서 해당 벤더 정보 조회 + const rfq: RfqWithItems | null = await findRfqById(idAsNumber) + + // 3) 사이드바 메뉴 + const sidebarNavItems = [ + { + title: "Matched Vendors", + href: `/${lng}/evcp/rfq/${id}`, + }, + { + title: "TBE", + href: `/${lng}/evcp/rfq/${id}/tbe`, + }, + { + title: "CBE", + href: `/${lng}/evcp/rfq/${id}/cbe`, + }, + + ] + + return ( + <> + <div className="container py-6"> + <section className="overflow-hidden rounded-[0.5rem] border bg-background shadow"> + <div className="hidden space-y-6 p-10 pb-16 md:block"> + <div className="space-y-0.5"> + {/* 4) 벤더 정보가 있으면 코드 + 이름 + "상세 정보" 표기 */} + <h2 className="text-2xl font-bold tracking-tight"> + {rfq + ? `${rfq.rfqCode ?? ""} 관리` + : "Loading RFQ..."} + </h2> + + <p className="text-muted-foreground"> + {rfq + ? `${rfq.description ?? ""} ${rfq.lines.map(line => line.itemCode).join(", ")}` + : ""} + </p> + <h3>Due Date:{ rfq && <strong>{formatDate(rfq?.dueDate)}</strong>}</h3> + </div> + <Separator className="my-6" /> + <div className="flex flex-col space-y-8 lg:flex-row lg:space-x-12 lg:space-y-0"> + <aside className="-mx-4 lg:w-1/6"> + <SidebarNav items={sidebarNavItems} /> + </aside> + <div className="flex-1">{children}</div> + </div> + </div> + </section> + </div> + </> + ) +}
\ No newline at end of file diff --git a/app/[lng]/evcp/rfq/[id]/page.tsx b/app/[lng]/evcp/rfq/[id]/page.tsx new file mode 100644 index 00000000..026ca5ac --- /dev/null +++ b/app/[lng]/evcp/rfq/[id]/page.tsx @@ -0,0 +1,55 @@ +import { Separator } from "@/components/ui/separator" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { getMatchedVendors } from "@/lib/rfqs/service" +import { searchParamsMatchedVCache } from "@/lib/rfqs/validations" +import { MatchedVendorsTable } from "@/lib/rfqs/vendor-table/vendors-table" + +interface IndexPageProps { + // Next.js 13 App Router에서 기본으로 주어지는 객체들 + params: { + lng: string + id: string + } + searchParams: Promise<SearchParams> +} + +export default async function RfqPage(props: IndexPageProps) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + const id = resolvedParams.id + + const idAsNumber = Number(id) + + // 2) SearchParams 파싱 (Zod) + // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 + const searchParams = await props.searchParams + const search = searchParamsMatchedVCache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getMatchedVendors({ + ...search, + filters: validFilters, + }, + idAsNumber) + ]) + + // 4) 렌더링 + return ( + <div className="space-y-6"> + <div> + <h3 className="text-lg font-medium"> + Vendors + </h3> + <p className="text-sm text-muted-foreground"> + 등록된 벤더 중에서 이 RFQ 아이템에 매칭되는 업체를 보여줍니다. <br/>"발행하기" 버튼을 통해 RFQ를 전송하면 첨부파일과 함께 RFQ 내용이 메일로 전달되고 eVCP에도 벤더가 입력할 수 있게 자동 생성됩니다. + </p> + </div> + <Separator /> + <div> + <MatchedVendorsTable promises={promises} rfqId={idAsNumber}/> + </div> + </div> + ) +}
\ No newline at end of file diff --git a/app/[lng]/evcp/rfq/[id]/tbe/page.tsx b/app/[lng]/evcp/rfq/[id]/tbe/page.tsx new file mode 100644 index 00000000..15c5d93c --- /dev/null +++ b/app/[lng]/evcp/rfq/[id]/tbe/page.tsx @@ -0,0 +1,55 @@ +import { Separator } from "@/components/ui/separator" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { getTBE } from "@/lib/rfqs/service" +import { searchParamsTBECache } from "@/lib/rfqs/validations" +import { TbeTable } from "@/lib/rfqs/tbe-table/tbe-table" + +interface IndexPageProps { + // Next.js 13 App Router에서 기본으로 주어지는 객체들 + params: { + lng: string + id: string + } + searchParams: Promise<SearchParams> +} + +export default async function RfqTBEPage(props: IndexPageProps) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + const id = resolvedParams.id + + const idAsNumber = Number(id) + + // 2) SearchParams 파싱 (Zod) + // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 + const searchParams = await props.searchParams + const search = searchParamsTBECache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getTBE({ + ...search, + filters: validFilters, + }, + idAsNumber) + ]) + + // 4) 렌더링 + return ( + <div className="space-y-6"> + <div> + <h3 className="text-lg font-medium"> + Technical Bid Evaluation + </h3> + <p className="text-sm text-muted-foreground"> + 초대된 벤더에게 TBE를 보낼 수 있습니다. <br/>"발행하기" 버튼을 통해 TBE를 전송하면 첨부파일과 함께 TBE 내용이 메일로 전달되고 eVCP에도 벤더가 입력할 수 있게 자동 생성됩니다. + </p> + </div> + <Separator /> + <div> + <TbeTable promises={promises} rfqId={idAsNumber}/> + </div> + </div> + ) +}
\ No newline at end of file diff --git a/app/[lng]/evcp/rfq/page.tsx b/app/[lng]/evcp/rfq/page.tsx new file mode 100644 index 00000000..3417b0bf --- /dev/null +++ b/app/[lng]/evcp/rfq/page.tsx @@ -0,0 +1,80 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" + +import { searchParamsCache } from "@/lib/rfqs/validations" +import { getRfqs, getRfqStatusCounts } from "@/lib/rfqs/service" +import { RfqsTable } from "@/lib/rfqs/table/rfqs-table" +import { getAllItems } from "@/lib/items/service" +import { RfqType } from "@/lib/rfqs/validations" + +interface RfqPageProps { + searchParams: Promise<SearchParams>; + rfqType: RfqType; + title: string; + description: string; +} + +export default async function RfqPage({ + searchParams, + rfqType = RfqType.PURCHASE, + title = "RFQ", + description = "RFQ를 등록하고 관리할 수 있습니다." +}: RfqPageProps) { + const search = searchParamsCache.parse(await searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getRfqs({ + ...search, + filters: validFilters, + rfqType // 전달받은 rfqType 사용 + }), + getRfqStatusCounts(rfqType), // rfqType 전달 + getAllItems() + ]) + + return ( + <Shell className="gap-2"> + <div className="flex items-center justify-between space-y-2"> + <div className="flex items-center justify-between space-y-2"> + <div> + <h2 className="text-2xl font-bold tracking-tight"> + {title} + </h2> + <p className="text-muted-foreground"> + {description} + </p> + </div> + </div> + </div> + + <React.Suspense fallback={<Skeleton className="h-7 w-52" />}> + {/* <DateRangePicker + triggerSize="sm" + triggerClassName="ml-auto w-56 sm:w-60" + align="end" + shallow={false} + /> */} + </React.Suspense> + <React.Suspense + fallback={ + <DataTableSkeleton + columnCount={6} + searchableColumnCount={1} + filterableColumnCount={2} + cellWidths={["10rem", "40rem", "12rem", "12rem", "8rem", "8rem"]} + shrinkZero + /> + } + > + <RfqsTable promises={promises} rfqType={rfqType} /> + </React.Suspense> + </Shell> + ) +}
\ No newline at end of file diff --git a/app/[lng]/evcp/settings/layout.tsx b/app/[lng]/evcp/settings/layout.tsx new file mode 100644 index 00000000..6f373567 --- /dev/null +++ b/app/[lng]/evcp/settings/layout.tsx @@ -0,0 +1,68 @@ +import { Metadata } from "next" + +import { Separator } from "@/components/ui/separator" +import { SidebarNav } from "@/components/layout/sidebar-nav" + +export const metadata: Metadata = { + title: "Settings", + // description: "Advanced form example using react-hook-form and Zod.", +} + + +interface SettingsLayoutProps { + children: React.ReactNode + params: { lng: string } +} + +export default async function SettingsLayout({ + children, + params, +}: { + children: React.ReactNode + params: { lng: string } +}) { + const resolvedParams = await params + const lng = resolvedParams.lng + + + const sidebarNavItems = [ + + { + title: "Account", + href: `/${lng}/evcp/settings`, + }, + { + title: "Preferences", + href: `/${lng}/evcp/settings/preferences`, + } + + + ] + + + return ( + <> + <div className="container py-6"> + <section className="overflow-hidden rounded-[0.5rem] border bg-background shadow"> + <div className="hidden space-y-6 p-10 pb-16 md:block"> + <div className="space-y-0.5"> + <h2 className="text-2xl font-bold tracking-tight">Settings</h2> + <p className="text-muted-foreground"> + Manage your account settings and preferences. + </p> + </div> + <Separator className="my-6" /> + <div className="flex flex-col space-y-8 lg:flex-row lg:space-x-12 lg:space-y-0"> + <aside className="-mx-4 lg:w-1/5"> + <SidebarNav items={sidebarNavItems} /> + </aside> + <div className="flex-1 ">{children}</div> + </div> + </div> + </section> + </div> + + + </> + ) +} diff --git a/app/[lng]/evcp/settings/page.tsx b/app/[lng]/evcp/settings/page.tsx new file mode 100644 index 00000000..a6eaac90 --- /dev/null +++ b/app/[lng]/evcp/settings/page.tsx @@ -0,0 +1,18 @@ +import { Separator } from "@/components/ui/separator" +import { AccountForm } from "@/components/settings/account-form" + +export default function SettingsAccountPage() { + return ( + <div className="space-y-6"> + <div> + <h3 className="text-lg font-medium">Account</h3> + <p className="text-sm text-muted-foreground"> + Update your account settings. Set your preferred language and + timezone. + </p> + </div> + <Separator /> + <AccountForm /> + </div> + ) +} diff --git a/app/[lng]/evcp/settings/preferences/page.tsx b/app/[lng]/evcp/settings/preferences/page.tsx new file mode 100644 index 00000000..e2a88021 --- /dev/null +++ b/app/[lng]/evcp/settings/preferences/page.tsx @@ -0,0 +1,17 @@ +import { Separator } from "@/components/ui/separator" +import { AppearanceForm } from "@/components/settings/appearance-form" + +export default function SettingsAppearancePage() { + return ( + <div className="space-y-6"> + <div> + <h3 className="text-lg font-medium">Preference</h3> + <p className="text-sm text-muted-foreground"> + Customize the preference of the app. + </p> + </div> + <Separator /> + <AppearanceForm /> + </div> + ) +} diff --git a/app/[lng]/evcp/system/admin-users/page.tsx b/app/[lng]/evcp/system/admin-users/page.tsx new file mode 100644 index 00000000..11a9e9fb --- /dev/null +++ b/app/[lng]/evcp/system/admin-users/page.tsx @@ -0,0 +1,60 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { DateRangePicker } from "@/components/date-range-picker" +import { Separator } from "@/components/ui/separator" + +import { searchParamsCache } from "@/lib/admin-users/validations" +import { getAllCompanies, getAllRoles, getUserCountGroupByCompany, getUserCountGroupByRole, getUsers } from "@/lib/admin-users/service" +import { AdmUserTable } from "@/lib/admin-users/table/ausers-table" + +interface IndexPageProps { + searchParams: Promise<SearchParams> +} + +export default async function UserTable(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = searchParamsCache.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getUsers({ + ...search, + filters: validFilters, + }), + getUserCountGroupByCompany(), + getUserCountGroupByRole(), + getAllCompanies(), + getAllRoles() + ]) + + return ( + <React.Suspense + fallback={ + <DataTableSkeleton + columnCount={6} + searchableColumnCount={1} + filterableColumnCount={2} + cellWidths={["10rem", "40rem", "12rem", "12rem", "8rem", "8rem"]} + shrinkZero + /> + } + > + <div className="space-y-6"> + <div> + <h3 className="text-lg font-medium">Vendor Admin User Management</h3> + <p className="text-sm text-muted-foreground"> + 협력업체의 유저 전체를 조회하고 어드민 유저를 생성할 수 있는 페이지입니다. 이곳에서 초기 유저를 생성시킬 수 있습니다. <br />생성 후에는 생성된 사용자의 이메일로 생성 통보 이메일이 발송되며 사용자는 이메일과 OTP로 로그인이 가능합니다. + </p> + </div> + <Separator /> + <AdmUserTable promises={promises} /> + </div> + </React.Suspense> + + ) +} diff --git a/app/[lng]/evcp/system/layout.tsx b/app/[lng]/evcp/system/layout.tsx new file mode 100644 index 00000000..4885a028 --- /dev/null +++ b/app/[lng]/evcp/system/layout.tsx @@ -0,0 +1,75 @@ +import { Metadata } from "next" + +import { Separator } from "@/components/ui/separator" +import { SidebarNav } from "@/components/layout/sidebar-nav" + +export const metadata: Metadata = { + title: "System Setting", + // description: "Advanced form example using react-hook-form and Zod.", +} + + +interface SettingsLayoutProps { + children: React.ReactNode + params: { lng: string } +} + +export default async function SettingsLayout({ + children, + params, +}: { + children: React.ReactNode + params: { lng: string } +}) { + const resolvedParams = await params + const lng = resolvedParams.lng + + + const sidebarNavItems = [ + + { + title: "Users", + href: `/${lng}/evcp/system`, + }, + { + title: "Roles", + href: `/${lng}/evcp/system/roles`, + }, + { + title: "Permissions", + href: `/${lng}/evcp/system/permissions`, + }, + { + title: "Vendor Users", + href: `/${lng}/evcp/system/admin-users`, + }, + + ] + + + return ( + <> + <div className="container py-6"> + <section className="overflow-hidden rounded-[0.5rem] border bg-background shadow"> + <div className="hidden space-y-6 p-10 pb-16 md:block"> + <div className="space-y-0.5"> + <h2 className="text-2xl font-bold tracking-tight">시스템 설정</h2> + <p className="text-muted-foreground"> + 사용자, 롤, 접근 권한을 관리하세요. + </p> + </div> + <Separator className="my-6" /> + <div className="flex flex-col space-y-8 lg:flex-row lg:space-x-12 lg:space-y-0"> + <aside className="-mx-4 lg:w-1/5"> + <SidebarNav items={sidebarNavItems} /> + </aside> + <div className="flex-1 ">{children}</div> + </div> + </div> + </section> + </div> + + + </> + ) +} diff --git a/app/[lng]/evcp/system/page.tsx b/app/[lng]/evcp/system/page.tsx new file mode 100644 index 00000000..2d180028 --- /dev/null +++ b/app/[lng]/evcp/system/page.tsx @@ -0,0 +1,56 @@ +import { Separator } from "@/components/ui/separator" +import { type SearchParams } from "@/types/table" +import * as React from "react" +import { getValidFilters } from "@/lib/data-table" +import { searchParamsCache } from "@/lib/admin-users/validations" +import { getAllRoles, getUsersEVCP } from "@/lib/users/service" +import { getUserCountGroupByRole } from "@/lib/admin-users/service" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { UserTable } from "@/lib/users/table/users-table" + +interface IndexPageProps { + searchParams: Promise<SearchParams> +} + +export default async function SystemUserPage(props: IndexPageProps) { + + const searchParams = await props.searchParams + const search = searchParamsCache.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getUsersEVCP({ + ...search, + filters: validFilters, + }), + getUserCountGroupByRole(), + getAllRoles() + ]) + + return ( + <React.Suspense + fallback={ + <DataTableSkeleton + columnCount={6} + searchableColumnCount={1} + filterableColumnCount={2} + cellWidths={["10rem", "12rem", "12rem", "12rem"]} + shrinkZero + /> + } + > + <div className="space-y-6"> + <div> + <h3 className="text-lg font-medium">Users</h3> + <p className="text-sm text-muted-foreground"> + 시스템 전체 사용자들을 조회하고 관리할 수 있는 페이지입니다. 사용자에게 롤을 할당하는 것으로 메뉴별 권한을 관리할 수 있습니다. + </p> + </div> + <Separator /> + <UserTable promises={promises} /> + </div> + </React.Suspense> + + ) +}
\ No newline at end of file diff --git a/app/[lng]/evcp/system/permissions/page.tsx b/app/[lng]/evcp/system/permissions/page.tsx new file mode 100644 index 00000000..6aa2b693 --- /dev/null +++ b/app/[lng]/evcp/system/permissions/page.tsx @@ -0,0 +1,17 @@ +import PermissionsTree from "@/components/system/permissionsTree" +import { Separator } from "@/components/ui/separator" + +export default function PermissionsPage() { + return ( + <div className="space-y-6"> + <div> + <h3 className="text-lg font-medium">Permissions</h3> + <p className="text-sm text-muted-foreground"> + Set permissions to the menu by Role + </p> + </div> + <Separator /> + <PermissionsTree/> + </div> + ) +} diff --git a/app/[lng]/evcp/system/roles/page.tsx b/app/[lng]/evcp/system/roles/page.tsx new file mode 100644 index 00000000..fe074600 --- /dev/null +++ b/app/[lng]/evcp/system/roles/page.tsx @@ -0,0 +1,68 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Separator } from "@/components/ui/separator" + +import { searchParamsCache } from "@/lib/roles/validations" +import { searchParamsCache as searchParamsCache2 } from "@/lib/admin-users/validations" +import { RolesTable } from "@/lib/roles/table/roles-table" +import { getRolesWithCount } from "@/lib/roles/services" +import { getUsersAll } from "@/lib/users/service" + +interface IndexPageProps { + searchParams: Promise<SearchParams> +} + +export default async function UserTable(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = searchParamsCache.parse(searchParams) + const search2 = searchParamsCache2.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getRolesWithCount({ + ...search, + filters: validFilters, + }), + + + ]) + + + const promises2 = Promise.all([ + getUsersAll({ + ...search2, + filters: validFilters, + }, "evcp"), + ]) + + + return ( + <React.Suspense + fallback={ + <DataTableSkeleton + columnCount={6} + searchableColumnCount={1} + filterableColumnCount={2} + cellWidths={["10rem", "40rem", "12rem", "12rem", "8rem", "8rem"]} + shrinkZero + /> + } + > + <div className="space-y-6"> + <div> + <h3 className="text-lg font-medium">Role Management</h3> + <p className="text-sm text-muted-foreground"> + 역할을 생성하고 역할에 유저를 할당할 수 있는 페이지입니다. 역할에 메뉴의 접근 권한 역시 할당할 수 있습니다. + </p> + </div> + <Separator /> + <RolesTable promises={promises} promises2={promises2} /> + </div> + </React.Suspense> + + ) +} diff --git a/app/[lng]/evcp/tag-numbering/page.tsx b/app/[lng]/evcp/tag-numbering/page.tsx new file mode 100644 index 00000000..9d5b903a --- /dev/null +++ b/app/[lng]/evcp/tag-numbering/page.tsx @@ -0,0 +1,74 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" +import { searchParamsCache } from "@/lib/tag-numbering/validation" +import { getTagNumbering } from "@/lib/tag-numbering/service" +import { TagNumberingTable } from "@/lib/tag-numbering/table/tagNumbering-table" + + +interface IndexPageProps { + searchParams: Promise<SearchParams> +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = searchParamsCache.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getTagNumbering({ + ...search, + filters: validFilters, + }), + + ]) + + return ( + <Shell className="gap-2"> + <div className="flex items-center justify-between space-y-2"> + <div className="flex items-center justify-between space-y-2"> + <div> + <h2 className="text-2xl font-bold tracking-tight"> + Tag Numbering from S-EDP + </h2> + <p className="text-muted-foreground"> + 태그 넘버링을 위한 룰셋을 S-EDP로부터 가져오고 확인할 수 있습니다{" "} + {/* <span className="inline-flex items-center whitespace-nowrap"> + <Ellipsis className="size-3" /> + <span className="ml-1">버튼</span> + </span> + 을 통해 담당자 연락처, 입찰 이력, 계약 이력, 패키지 내용 등을 확인 할 수 있습니다. */} + </p> + </div> + </div> + </div> + + <React.Suspense fallback={<Skeleton className="h-7 w-52" />}> + {/* <DateRangePicker + triggerSize="sm" + triggerClassName="ml-auto w-56 sm:w-60" + align="end" + shallow={false} + /> */} + </React.Suspense> + <React.Suspense + fallback={ + <DataTableSkeleton + columnCount={6} + searchableColumnCount={1} + filterableColumnCount={2} + cellWidths={["10rem", "40rem", "12rem", "12rem", "8rem", "8rem"]} + shrinkZero + /> + } + > + <TagNumberingTable promises={promises} /> + </React.Suspense> + </Shell> + ) +} diff --git a/app/[lng]/evcp/tasks/page.tsx b/app/[lng]/evcp/tasks/page.tsx new file mode 100644 index 00000000..f14cc757 --- /dev/null +++ b/app/[lng]/evcp/tasks/page.tsx @@ -0,0 +1,63 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { DateRangePicker } from "@/components/date-range-picker" +import { Shell } from "@/components/shell" + +import { FeatureFlagsProvider } from "@/lib/tasks/table/feature-flags-provider" +import { TasksTable } from "@/lib/tasks/table/tasks-table" +import { + getTaskPriorityCounts, + getTasks, + getTaskStatusCounts, +} from "@/lib/tasks/service" +import { searchParamsCache } from "@/lib/tasks/validations" + +interface IndexPageProps { + searchParams: Promise<SearchParams> +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = searchParamsCache.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getTasks({ + ...search, + filters: validFilters, + }), + getTaskStatusCounts(), + getTaskPriorityCounts(), + ]) + + return ( + <Shell className="gap-2"> + <React.Suspense fallback={<Skeleton className="h-7 w-52" />}> + {/* <DateRangePicker + triggerSize="sm" + triggerClassName="ml-auto w-56 sm:w-60" + align="end" + shallow={false} + /> */} + </React.Suspense> + <React.Suspense + fallback={ + <DataTableSkeleton + columnCount={6} + searchableColumnCount={1} + filterableColumnCount={2} + cellWidths={["10rem", "40rem", "12rem", "12rem", "8rem", "8rem"]} + shrinkZero + /> + } + > + <TasksTable promises={promises} /> + </React.Suspense> + </Shell> + ) +} diff --git a/app/[lng]/evcp/vendors/[id]/info/items/page.tsx b/app/[lng]/evcp/vendors/[id]/info/items/page.tsx new file mode 100644 index 00000000..e9ff17b4 --- /dev/null +++ b/app/[lng]/evcp/vendors/[id]/info/items/page.tsx @@ -0,0 +1,56 @@ +import { Separator } from "@/components/ui/separator" +import { getVendorItems } from "@/lib/vendors/service" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { searchParamsItemCache } from "@/lib/vendors/validations" +import { VendorItemsTable } from "@/lib/vendors/items-table/item-table" + +interface IndexPageProps { + // Next.js 13 App Router에서 기본으로 주어지는 객체들 + params: { + lng: string + id: string + } + searchParams: Promise<SearchParams> +} + +export default async function SettingsAccountPage(props: IndexPageProps) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + const id = resolvedParams.id + + const idAsNumber = Number(id) + + // 2) SearchParams 파싱 (Zod) + // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 + const searchParams = await props.searchParams + const search = searchParamsItemCache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + + + const promises = Promise.all([ + getVendorItems({ + ...search, + filters: validFilters, + }, + idAsNumber) + ]) + // 4) 렌더링 + return ( + <div className="space-y-6"> + <div> + <h3 className="text-lg font-medium"> + Possible Items + </h3> + <p className="text-sm text-muted-foreground"> + 딜리버리가 가능한 아이템 리스트를 확인할 수 있습니다. + </p> + </div> + <Separator /> + <div> + <VendorItemsTable promises={promises} vendorId={idAsNumber}/> + </div> + </div> + ) +}
\ No newline at end of file diff --git a/app/[lng]/evcp/vendors/[id]/info/layout.tsx b/app/[lng]/evcp/vendors/[id]/info/layout.tsx new file mode 100644 index 00000000..39e0bac0 --- /dev/null +++ b/app/[lng]/evcp/vendors/[id]/info/layout.tsx @@ -0,0 +1,79 @@ +import { Metadata } from "next" + +import { Separator } from "@/components/ui/separator" +import { SidebarNav } from "@/components/layout/sidebar-nav" +import { findVendorById } from "@/lib/vendors/service" // 가정: 여기에 findVendorById가 있다고 가정 +import { Vendor } from "@/db/schema/vendors" + +export const metadata: Metadata = { + title: "Vendor Detail", +} + +export default async function SettingsLayout({ + children, + params, +}: { + children: React.ReactNode + params: { lng: string , id: string} +}) { + + // 1) URL 파라미터에서 id 추출, Number로 변환 + const resolvedParams = await params + const lng = resolvedParams.lng + const id = resolvedParams.id + + const idAsNumber = Number(id) + // 2) DB에서 해당 벤더 정보 조회 + const vendor: Vendor | null = await findVendorById(idAsNumber) + + // 3) 사이드바 메뉴 + const sidebarNavItems = [ + { + title: "Contacts", + href: `/${lng}/evcp/vendors/${id}/info`, + }, + { + title: "Items", + href: `/${lng}/evcp/vendors/${id}/info/items`, + }, + { + title: "RFQ History", + href: `/${lng}/evcp/vendors/${id}/info/rfq-history`, + }, + { + title: "Bidding History", + href: `/${lng}/evcp/vendors/${id}/info/bid-history`, + }, + { + title: "Contract History", + href: `/${lng}/evcp/vendors/${id}/info/contract-history`, + }, + ] + + return ( + <> + <div className="container py-6"> + <section className="overflow-hidden rounded-[0.5rem] border bg-background shadow"> + <div className="hidden space-y-6 p-10 pb-16 md:block"> + <div className="space-y-0.5"> + {/* 4) 벤더 정보가 있으면 코드 + 이름 + "상세 정보" 표기 */} + <h2 className="text-2xl font-bold tracking-tight"> + {vendor + ? `${vendor.vendorCode ?? ""} - ${vendor.vendorName} 상세 정보` + : "Loading Vendor..."} + </h2> + <p className="text-muted-foreground">벤더 관련 상세사항을 확인하세요.</p> + </div> + <Separator className="my-6" /> + <div className="flex flex-col space-y-8 lg:flex-row lg:space-x-12 lg:space-y-0"> + <aside className="-mx-4 lg:w-1/5"> + <SidebarNav items={sidebarNavItems} /> + </aside> + <div className="flex-1">{children}</div> + </div> + </div> + </section> + </div> + </> + ) +}
\ No newline at end of file diff --git a/app/[lng]/evcp/vendors/[id]/info/page.tsx b/app/[lng]/evcp/vendors/[id]/info/page.tsx new file mode 100644 index 00000000..6279e924 --- /dev/null +++ b/app/[lng]/evcp/vendors/[id]/info/page.tsx @@ -0,0 +1,56 @@ +import { Separator } from "@/components/ui/separator" +import { getVendorContacts } from "@/lib/vendors/service" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { searchParamsContactCache } from "@/lib/vendors/validations" +import { VendorContactsTable } from "@/lib/vendors/contacts-table/contact-table" + +interface IndexPageProps { + // Next.js 13 App Router에서 기본으로 주어지는 객체들 + params: { + lng: string + id: string + } + searchParams: Promise<SearchParams> +} + +export default async function SettingsAccountPage(props: IndexPageProps) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + const id = resolvedParams.id + + const idAsNumber = Number(id) + + // 2) SearchParams 파싱 (Zod) + // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 + const searchParams = await props.searchParams + const search = searchParamsContactCache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + + + const promises = Promise.all([ + getVendorContacts({ + ...search, + filters: validFilters, + }, + idAsNumber) + ]) + // 4) 렌더링 + return ( + <div className="space-y-6"> + <div> + <h3 className="text-lg font-medium"> + Contacts + </h3> + <p className="text-sm text-muted-foreground"> + 업무별 담당자 정보를 확인하세요. + </p> + </div> + <Separator /> + <div> + <VendorContactsTable promises={promises} vendorId={idAsNumber}/> + </div> + </div> + ) +}
\ No newline at end of file diff --git a/app/[lng]/evcp/vendors/[id]/info/rfq-history/page.tsx b/app/[lng]/evcp/vendors/[id]/info/rfq-history/page.tsx new file mode 100644 index 00000000..1d2f618c --- /dev/null +++ b/app/[lng]/evcp/vendors/[id]/info/rfq-history/page.tsx @@ -0,0 +1,55 @@ +import { Separator } from "@/components/ui/separator"
+import { getRfqHistory } from "@/lib/vendors/service"
+import { type SearchParams } from "@/types/table"
+import { getValidFilters } from "@/lib/data-table"
+import { searchParamsRfqHistoryCache } from "@/lib/vendors/validations"
+import { VendorRfqHistoryTable } from "@/lib/vendors/rfq-history-table/rfq-history-table"
+
+interface IndexPageProps {
+ // Next.js 13 App Router에서 기본으로 주어지는 객체들
+ params: {
+ lng: string
+ id: string
+ }
+ searchParams: Promise<SearchParams>
+}
+
+export default async function RfqHistoryPage(props: IndexPageProps) {
+ const resolvedParams = await props.params
+ const lng = resolvedParams.lng
+ const id = resolvedParams.id
+
+ const idAsNumber = Number(id)
+
+ // 2) SearchParams 파싱 (Zod)
+ // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼
+ const searchParams = await props.searchParams
+ const search = searchParamsRfqHistoryCache.parse(searchParams)
+ const validFilters = getValidFilters(search.filters)
+
+ const promises = Promise.all([
+ getRfqHistory({
+ ...search,
+ filters: validFilters,
+ },
+ idAsNumber)
+ ])
+
+ // 4) 렌더링
+ return (
+ <div className="space-y-6">
+ <div>
+ <h3 className="text-lg font-medium">
+ RFQ History
+ </h3>
+ <p className="text-sm text-muted-foreground">
+ 벤더의 RFQ 참여 이력을 확인할 수 있습니다.
+ </p>
+ </div>
+ <Separator />
+ <div>
+ <VendorRfqHistoryTable promises={promises} />
+ </div>
+ </div>
+ )
+}
\ No newline at end of file diff --git a/app/[lng]/evcp/vendors/page.tsx b/app/[lng]/evcp/vendors/page.tsx new file mode 100644 index 00000000..e3cc7fdc --- /dev/null +++ b/app/[lng]/evcp/vendors/page.tsx @@ -0,0 +1,78 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" + + +import { searchParamsCache } from "@/lib/vendors/validations" +import { getVendors, getVendorStatusCounts } from "@/lib/vendors/service" +import { VendorsTable } from "@/lib/vendors/table/vendors-table" +import { Ellipsis } from "lucide-react" + +interface IndexPageProps { + searchParams: Promise<SearchParams> +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = searchParamsCache.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getVendors({ + ...search, + filters: validFilters, + }), + getVendorStatusCounts(), + ]) + + return ( + <Shell className="gap-2"> + + <div className="flex items-center justify-between space-y-2"> + <div className="flex items-center justify-between space-y-2"> + <div> + <h2 className="text-2xl font-bold tracking-tight"> + Vendor Information + </h2> + <p className="text-muted-foreground"> + 벤더에 대한 요약 정보를 확인하고{" "} + <span className="inline-flex items-center whitespace-nowrap"> + <Ellipsis className="size-3" /> + <span className="ml-1">버튼</span> + </span> + 을 통해 담당자 연락처, 입찰 이력, 계약 이력, 패키지 내용 등을 확인 할 수 있습니다. <br/>벤더의 상태에 따라 가입을 승인해주거나 PQ 요청을 할 수 있고 검토가 완료된 벤더를 기간계 시스템에 전송하여 벤더 코드를 따올 수 있습니다. + </p> + </div> + </div> + </div> + + + <React.Suspense fallback={<Skeleton className="h-7 w-52" />}> + {/* <DateRangePicker + triggerSize="sm" + triggerClassName="ml-auto w-56 sm:w-60" + align="end" + shallow={false} + /> */} + </React.Suspense> + <React.Suspense + fallback={ + <DataTableSkeleton + columnCount={6} + searchableColumnCount={1} + filterableColumnCount={2} + cellWidths={["10rem", "40rem", "12rem", "12rem", "8rem", "8rem"]} + shrinkZero + /> + } + > + <VendorsTable promises={promises} /> + </React.Suspense> + </Shell> + ) +} diff --git a/app/[lng]/login/page.tsx b/app/[lng]/login/page.tsx new file mode 100644 index 00000000..0e3a498c --- /dev/null +++ b/app/[lng]/login/page.tsx @@ -0,0 +1,15 @@ +import { LoginForm } from "@/components/login/login-form" +import { Suspense } from "react" + +export default function LoginPage() { + return ( + <Suspense fallback={<div>Loading login form...</div>}> + <LoginForm /> + </Suspense> + // <div className="flex min-h-svh flex-col items-center justify-center bg-muted p-6 md:p-10"> + // <div className="w-full max-w-sm md:max-w-3xl"> + + // </div> + // </div> + ) +} diff --git a/app/[lng]/partners/(partners)/dashboard/page.tsx b/app/[lng]/partners/(partners)/dashboard/page.tsx new file mode 100644 index 00000000..a1e9f8be --- /dev/null +++ b/app/[lng]/partners/(partners)/dashboard/page.tsx @@ -0,0 +1,8 @@ + +export default function Pages() { + return ( + <> + test + </> + ) + }
\ No newline at end of file diff --git a/app/[lng]/partners/(partners)/document-list/[contractId]/page.tsx b/app/[lng]/partners/(partners)/document-list/[contractId]/page.tsx new file mode 100644 index 00000000..65df0b1f --- /dev/null +++ b/app/[lng]/partners/(partners)/document-list/[contractId]/page.tsx @@ -0,0 +1,44 @@ +import { Separator } from "@/components/ui/separator" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { searchParamsCache } from "@/lib/vendor-document-list/validations" +import { getVendorDocuments } from "@/lib/vendor-document-list/service" +import { DocumentsTable } from "@/lib/vendor-document-list/table/doc-table" + +interface IndexPageProps { + params: { + contractId: string // Updated from 'id' to 'contractId' to match route parameter + } + searchParams: Promise<SearchParams> +} + +export default async function DocumentListPage(props: IndexPageProps) { + const resolvedParams = await props.params + const contractId = resolvedParams.contractId // Updated from 'id' to 'contractId' + + const idAsNumber = Number(contractId) + + // 2) SearchParams 파싱 (Zod) + // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 + const searchParams = await props.searchParams + const search = searchParamsCache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + const projectType = searchParams.projectType === "plant" ? "plant" : "ship" + + const promises = Promise.all([ + getVendorDocuments({ + ...search, + filters: validFilters, + }, idAsNumber) + ]) + + // 4) 렌더링 + return ( + <div className="space-y-6"> + <div> + <DocumentsTable promises={promises} selectedPackageId={idAsNumber} projectType={projectType}/> + </div> + </div> + ) +}
\ No newline at end of file diff --git a/app/[lng]/partners/(partners)/document-list/layout.tsx b/app/[lng]/partners/(partners)/document-list/layout.tsx new file mode 100644 index 00000000..a75cdf7d --- /dev/null +++ b/app/[lng]/partners/(partners)/document-list/layout.tsx @@ -0,0 +1,45 @@ + +import { cookies } from "next/headers" +import { Shell } from "@/components/shell" +import DocumentContainer from "@/components/documents/document-container" +import { getVendorProjectsAndContracts } from "@/lib/vendor-data/services" +import { getVendorDocumentLists } from "@/lib/vendor-document/service" +import VendorDocumentsClient from "@/components/documents/vendor-docs.client" +import VendorDocumentListClient from "@/components/document-lists/vendor-doc-list-client" + + + +// Layout 컴포넌트는 서버 컴포넌트입니다 +export default async function VendorDocuments({ + children, +}: { + children: React.ReactNode +}) { + // const session = await getServerSession(authOptions) + // const vendorId = session?.user.companyId + const vendorId = "17" + const idAsNumber = Number(vendorId) + + const projects = await getVendorProjectsAndContracts(idAsNumber) + + + // 레이아웃 설정 쿠키 가져오기 + // Next.js 15에서는 cookies()가 Promise를 반환하므로 await 사용 + const cookieStore = await cookies() + + // 이제 cookieStore.get() 메서드 사용 가능 + const layout = cookieStore.get("react-resizable-panels:layout:mail") + const collapsed = cookieStore.get("react-resizable-panels:collapsed") + + const defaultLayout = layout ? JSON.parse(layout.value) : undefined + const defaultCollapsed = collapsed ? JSON.parse(collapsed.value) : undefined + + + return ( + <Shell className="gap-2"> + <VendorDocumentListClient projects={projects}> + {children} + </VendorDocumentListClient> + </Shell> + ) +}
\ No newline at end of file diff --git a/app/[lng]/partners/(partners)/document-list/page.tsx b/app/[lng]/partners/(partners)/document-list/page.tsx new file mode 100644 index 00000000..721eb408 --- /dev/null +++ b/app/[lng]/partners/(partners)/document-list/page.tsx @@ -0,0 +1,21 @@ +// app/vendor-data/page.tsx +import * as React from "react" +import { Separator } from "@/components/ui/separator" + +export default async function IndexPage() { + return ( + <div className="space-y-6"> + + <div className="grid gap-4"> + <div className="rounded-lg border p-4"> + <h4 className="text-sm font-medium">시작하는 방법</h4> + <p className="text-sm text-muted-foreground mt-1"> + 오른쪽 상단에서 프로젝트/계약을 선택하세요.<br /> + + + </p> + </div> + </div> + </div> + ) +}
\ No newline at end of file diff --git a/app/[lng]/partners/(partners)/documents/[contractId]/page.tsx b/app/[lng]/partners/(partners)/documents/[contractId]/page.tsx new file mode 100644 index 00000000..7bf50c15 --- /dev/null +++ b/app/[lng]/partners/(partners)/documents/[contractId]/page.tsx @@ -0,0 +1,47 @@ +import { Separator } from "@/components/ui/separator" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { TagsTable } from "@/lib/tags/table/tag-table" +import { searchParamsCache } from "@/lib/vendor-document/validations" +import { getTags } from "@/lib/tags/service" +import { getVendorDocumentLists } from "@/lib/vendor-document/service" +import { DocumentListTable } from "@/lib/vendor-document/table/doc-table" +import DocumentContainer from "@/components/documents/document-container" + +interface IndexPageProps { + params: { + contractId: string // Updated from 'id' to 'contractId' to match route parameter + } + searchParams: Promise<SearchParams> +} + +export default async function DocumentListPage(props: IndexPageProps) { + const resolvedParams = await props.params + const contractId = resolvedParams.contractId // Updated from 'id' to 'contractId' + + const idAsNumber = Number(contractId) + + console.log(idAsNumber) + + // 2) SearchParams 파싱 (Zod) + // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 + const searchParams = await props.searchParams + const search = searchParamsCache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getVendorDocumentLists({ + ...search, + filters: validFilters, + }, idAsNumber) + ]) + + // 4) 렌더링 + return ( + <div className="space-y-6"> + <div> + <DocumentContainer promises={promises} selectedPackageId={idAsNumber}/> + </div> + </div> + ) +}
\ No newline at end of file diff --git a/app/[lng]/partners/(partners)/documents/layout.tsx b/app/[lng]/partners/(partners)/documents/layout.tsx new file mode 100644 index 00000000..3ac0c573 --- /dev/null +++ b/app/[lng]/partners/(partners)/documents/layout.tsx @@ -0,0 +1,44 @@ + +import { cookies } from "next/headers" +import { Shell } from "@/components/shell" +import DocumentContainer from "@/components/documents/document-container" +import { getVendorProjectsAndContracts } from "@/lib/vendor-data/services" +import { getVendorDocumentLists } from "@/lib/vendor-document/service" +import VendorDocumentsClient from "@/components/documents/vendor-docs.client" + + + +// Layout 컴포넌트는 서버 컴포넌트입니다 +export default async function VendorDocuments({ + children, +}: { + children: React.ReactNode +}) { + // const session = await getServerSession(authOptions) + // const vendorId = session?.user.companyId + const vendorId = "17" + const idAsNumber = Number(vendorId) + + const projects = await getVendorProjectsAndContracts(idAsNumber) + + + // 레이아웃 설정 쿠키 가져오기 + // Next.js 15에서는 cookies()가 Promise를 반환하므로 await 사용 + const cookieStore = await cookies() + + // 이제 cookieStore.get() 메서드 사용 가능 + const layout = cookieStore.get("react-resizable-panels:layout:mail") + const collapsed = cookieStore.get("react-resizable-panels:collapsed") + + const defaultLayout = layout ? JSON.parse(layout.value) : undefined + const defaultCollapsed = collapsed ? JSON.parse(collapsed.value) : undefined + + + return ( + <Shell className="gap-2"> + <VendorDocumentsClient projects={projects}> + {children} + </VendorDocumentsClient> + </Shell> + ) +}
\ No newline at end of file diff --git a/app/[lng]/partners/(partners)/documents/page.tsx b/app/[lng]/partners/(partners)/documents/page.tsx new file mode 100644 index 00000000..721eb408 --- /dev/null +++ b/app/[lng]/partners/(partners)/documents/page.tsx @@ -0,0 +1,21 @@ +// app/vendor-data/page.tsx +import * as React from "react" +import { Separator } from "@/components/ui/separator" + +export default async function IndexPage() { + return ( + <div className="space-y-6"> + + <div className="grid gap-4"> + <div className="rounded-lg border p-4"> + <h4 className="text-sm font-medium">시작하는 방법</h4> + <p className="text-sm text-muted-foreground mt-1"> + 오른쪽 상단에서 프로젝트/계약을 선택하세요.<br /> + + + </p> + </div> + </div> + </div> + ) +}
\ No newline at end of file diff --git a/app/[lng]/partners/(partners)/layout.tsx b/app/[lng]/partners/(partners)/layout.tsx new file mode 100644 index 00000000..9dc39f7b --- /dev/null +++ b/app/[lng]/partners/(partners)/layout.tsx @@ -0,0 +1,17 @@ +import { ReactNode } from 'react'; +import { Header } from '@/components/layout/Header'; +import { SiteFooter } from '@/components/layout/Footer'; + +export default function EvcpLayout({ children }: { children: ReactNode }) { + return ( + <div className="relative flex min-h-svh flex-col bg-background"> + <Header /> + <main className="flex flex-1 flex-col"> + <div className='container-wrapper'> + {children} + </div> + </main> + <SiteFooter/> + </div> + ); +}
\ No newline at end of file diff --git a/app/[lng]/partners/(partners)/rfq/page.tsx b/app/[lng]/partners/(partners)/rfq/page.tsx new file mode 100644 index 00000000..34b66115 --- /dev/null +++ b/app/[lng]/partners/(partners)/rfq/page.tsx @@ -0,0 +1,133 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" +import { searchParamsRfqsForVendorsCache } from "@/lib/rfqs/validations" +import { RfqsVendorTable } from "@/lib/vendor-rfq-response/vendor-rfq-table/rfqs-table" +import { getServerSession } from "next-auth" +import { authOptions } from "@/app/api/auth/[...nextauth]/route" +import Link from "next/link" +import { Button } from "@/components/ui/button" +import { LogIn } from "lucide-react" +import { getRfqResponsesForVendor } from "@/lib/vendor-rfq-response/service" + +interface IndexPageProps { + searchParams: Promise<SearchParams> +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = searchParamsRfqsForVendorsCache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + // Get session + const session = await getServerSession(authOptions) + + // Check if user is logged in + if (!session || !session.user) { + // Return login required UI instead of redirecting + return ( + <Shell className="gap-6"> + <div className="flex items-center justify-between"> + <div> + <h2 className="text-2xl font-bold tracking-tight"> + RFQ + </h2> + <p className="text-muted-foreground"> + RFQ를 응답하고 커뮤니케이션을 할 수 있습니다. + </p> + </div> + </div> + + <div className="flex flex-col items-center justify-center py-12 text-center"> + <div className="rounded-lg border border-dashed p-10 shadow-sm"> + <h3 className="mb-2 text-xl font-semibold">로그인이 필요합니다</h3> + <p className="mb-6 text-muted-foreground"> + RFQ를 확인하려면 먼저 로그인하세요. + </p> + <Button size="lg" asChild> + <Link href="/partners"> + <LogIn className="mr-2 h-4 w-4" /> + 로그인하기 + </Link> + </Button> + </div> + </div> + </Shell> + ) + } + + // User is logged in, proceed with vendor ID + const vendorId = session.user.companyId + + // Validate vendorId (should be a number) + const idAsNumber = Number(vendorId) + + if (isNaN(idAsNumber)) { + // Handle invalid vendor ID (this shouldn't happen if authentication is working properly) + return ( + <Shell className="gap-6"> + <div className="flex items-center justify-between"> + <div> + <h2 className="text-2xl font-bold tracking-tight"> + RFQ + </h2> + </div> + </div> + <div className="flex flex-col items-center justify-center py-12 text-center"> + <div className="rounded-lg border border-dashed p-10 shadow-sm"> + <h3 className="mb-2 text-xl font-semibold">계정 오류</h3> + <p className="mb-6 text-muted-foreground"> + 업체 정보가 올바르게 설정되지 않았습니다. 관리자에게 문의하세요. + </p> + </div> + </div> + </Shell> + ) + } + + // If we got here, we have a valid vendor ID + const promises = Promise.all([ + getRfqResponsesForVendor({ + ...search, + filters: validFilters, + }, idAsNumber) + ]) + + return ( + <Shell className="gap-2"> + <div className="flex items-center justify-between space-y-2"> + <div className="flex items-center justify-between space-y-2"> + <div> + <h2 className="text-2xl font-bold tracking-tight"> + RFQ + </h2> + <p className="text-muted-foreground"> + RFQ를 응답하고 커뮤니케이션을 할 수 있습니다. + </p> + </div> + </div> + </div> + + <React.Suspense fallback={<Skeleton className="h-7 w-52" />}> + {/* DateRangePicker can go here */} + </React.Suspense> + + <React.Suspense + fallback={ + <DataTableSkeleton + columnCount={6} + searchableColumnCount={1} + filterableColumnCount={2} + cellWidths={["10rem", "40rem", "12rem", "12rem", "8rem", "8rem"]} + shrinkZero + /> + } + > + <RfqsVendorTable promises={promises} /> + </React.Suspense> + </Shell> + ) +}
\ No newline at end of file diff --git a/app/[lng]/partners/(partners)/system/page.tsx b/app/[lng]/partners/(partners)/system/page.tsx new file mode 100644 index 00000000..a1e9f8be --- /dev/null +++ b/app/[lng]/partners/(partners)/system/page.tsx @@ -0,0 +1,8 @@ + +export default function Pages() { + return ( + <> + test + </> + ) + }
\ No newline at end of file diff --git a/app/[lng]/partners/(partners)/tbe/page.tsx b/app/[lng]/partners/(partners)/tbe/page.tsx new file mode 100644 index 00000000..ab51659c --- /dev/null +++ b/app/[lng]/partners/(partners)/tbe/page.tsx @@ -0,0 +1,85 @@ +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { getTBEforVendor } from "@/lib/rfqs/service" +import { searchParamsTBECache } from "@/lib/rfqs/validations" +import { getServerSession } from "next-auth" +import { authOptions } from "@/app/api/auth/[...nextauth]/route" +import { TbeVendorTable } from "@/lib/vendor-rfq-response/vendor-tbe-table/tbe-table" +import * as React from "react" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" + +interface IndexPageProps { + // Next.js 13 App Router에서 기본으로 주어지는 객체들 + params: { + lng: string + id: string + } + searchParams: Promise<SearchParams> +} + +export default async function RfqTBEPage(props: IndexPageProps) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + + // 2) SearchParams 파싱 (Zod) + // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 + const searchParams = await props.searchParams + const search = searchParamsTBECache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + const session = await getServerSession(authOptions) + const vendorId = session?.user.companyId + // const vendorId = "17" + + const idAsNumber = Number(vendorId) + + const promises = Promise.all([ + getTBEforVendor({ + ...search, + filters: validFilters, + }, + idAsNumber) + ]) + + + return ( + <Shell className="gap-2"> + <div className="flex items-center justify-between space-y-2"> + <div className="flex items-center justify-between space-y-2"> + <div> + <h2 className="text-2xl font-bold tracking-tight"> + Technical Bid Evaluation + </h2> + <p className="text-sm text-muted-foreground"> + TBE에 응답하고 커뮤니케이션을 할 수 있습니다.{" "} + </p> + </div> + </div> + </div> + + <React.Suspense fallback={<Skeleton className="h-7 w-52" />}> + {/* <DateRangePicker + triggerSize="sm" + triggerClassName="ml-auto w-56 sm:w-60" + align="end" + shallow={false} + /> */} + </React.Suspense> + <React.Suspense + fallback={ + <DataTableSkeleton + columnCount={6} + searchableColumnCount={1} + filterableColumnCount={2} + cellWidths={["10rem", "40rem", "12rem", "12rem", "8rem", "8rem"]} + shrinkZero + /> + } + > + <TbeVendorTable promises={promises} /> + </React.Suspense> + </Shell> + ) +} diff --git a/app/[lng]/partners/(partners)/vendor-data/form/[packageId]/[formId]/page.tsx b/app/[lng]/partners/(partners)/vendor-data/form/[packageId]/[formId]/page.tsx new file mode 100644 index 00000000..248bd7fc --- /dev/null +++ b/app/[lng]/partners/(partners)/vendor-data/form/[packageId]/[formId]/page.tsx @@ -0,0 +1,41 @@ +import DynamicTable from "@/components/form-data/form-data-table" +import { getFormData } from "@/lib/forms/services" + +interface IndexPageProps { + params: { + lng: string + packageId: string + formId: string + } +} + +export default async function FormPage({ params }: IndexPageProps) { + // 1) 구조 분해 할당 + const resolvedParams = await params + + // 2) 구조 분해 할당 + const { lng, packageId, formId } = resolvedParams + + // 2) 변환 + const packageIdAsNumber = Number(packageId) + + // 3) DB 조회 + const { columns, data } = await getFormData(formId, packageIdAsNumber) + + // 4) 예외 처리 + if (!columns) { + return <p className="text-red-500">해당 폼의 메타 정보를 불러올 수 없습니다.</p> + } + + // 5) 렌더링 + return ( + <div className="space-y-6"> + <DynamicTable + contractItemId={packageIdAsNumber} + formCode={formId} + columnsJSON={columns} + dataJSON={data} + /> + </div> + ) +}
\ No newline at end of file diff --git a/app/[lng]/partners/(partners)/vendor-data/layout.tsx b/app/[lng]/partners/(partners)/vendor-data/layout.tsx new file mode 100644 index 00000000..a8b51c52 --- /dev/null +++ b/app/[lng]/partners/(partners)/vendor-data/layout.tsx @@ -0,0 +1,69 @@ +// app/vendor-data/layout.tsx +import * as React from "react" +import { cookies } from "next/headers" +import { Shell } from "@/components/shell" +import { getVendorProjectsAndContracts } from "@/lib/vendor-data/services" +import { VendorDataContainer } from "@/components/vendor-data/vendor-data-container" + +// Layout 컴포넌트는 서버 컴포넌트입니다 +export default async function VendorDataLayout({ + children, +}: { + children: React.ReactNode +}) { + // const session = await getServerSession(authOptions) + // const vendorId = session?.user.companyId + const vendorId = "17" + const idAsNumber = Number(vendorId) + + // 프로젝트 데이터 가져오기 + const projects = await getVendorProjectsAndContracts(idAsNumber) + + // 레이아웃 설정 쿠키 가져오기 + // Next.js 15에서는 cookies()가 Promise를 반환하므로 await 사용 + const cookieStore = await cookies() + + // 이제 cookieStore.get() 메서드 사용 가능 + const layout = cookieStore.get("react-resizable-panels:layout:mail") + const collapsed = cookieStore.get("react-resizable-panels:collapsed") + + const defaultLayout = layout ? JSON.parse(layout.value) : undefined + const defaultCollapsed = collapsed ? JSON.parse(collapsed.value) : undefined + + return ( + <Shell className="gap-2"> + <div className="flex items-center justify-between space-y-2"> + <div className="flex items-center justify-between space-y-2"> + <div> + <h2 className="text-2xl font-bold tracking-tight"> + Vendor Data + </h2> + <p className="text-muted-foreground"> + 각종 Data 입력할 수 있습니다 + </p> + </div> + </div> + </div> + + <section className="overflow-hidden rounded-[0.5rem] border bg-background shadow"> + <div className="hidden flex-col md:flex"> + {projects.length === 0 ? ( + <div className="p-4 text-center text-sm text-muted-foreground"> + No projects found for this vendor. + </div> + ) : ( + <VendorDataContainer + projects={projects} + defaultLayout={defaultLayout} + defaultCollapsed={defaultCollapsed} + navCollapsedSize={4} + > + {/* 페이지별 콘텐츠가 여기에 들어갑니다 */} + {children} + </VendorDataContainer> + )} + </div> + </section> + </Shell> + ) +}
\ No newline at end of file diff --git a/app/[lng]/partners/(partners)/vendor-data/page.tsx b/app/[lng]/partners/(partners)/vendor-data/page.tsx new file mode 100644 index 00000000..3eead226 --- /dev/null +++ b/app/[lng]/partners/(partners)/vendor-data/page.tsx @@ -0,0 +1,29 @@ +// app/vendor-data/page.tsx +import * as React from "react" +import { Separator } from "@/components/ui/separator" + +export default async function IndexPage() { + return ( + <div className="space-y-6"> + <div> + <h3 className="text-lg font-medium">벤더 데이터 대시보드</h3> + <p className="text-sm text-muted-foreground"> + 왼쪽 사이드바에서 패키지를 선택하여 태그를 관리하세요. + </p> + </div> + <Separator /> + <div className="grid gap-4"> + <div className="rounded-lg border p-4"> + <h4 className="text-sm font-medium">시작하는 방법</h4> + <p className="text-sm text-muted-foreground mt-1"> + 1. 왼쪽 상단에서 프로젝트/계약을 선택하세요.<br /> + 2. 사이드바에서 패키지 항목을 클릭하세요.<br /> + 3. 선택한 패키지의 태그 정보를 확인하고 관리할 수 있습니다.<br /> + 4. 사이드바에서 폼 항목을 클릭하세요.<br /> + 5. 선택함 폼의 칼럼 정보를 확인하고 관리할 수 있습니다. + </p> + </div> + </div> + </div> + ) +}
\ No newline at end of file diff --git a/app/[lng]/partners/(partners)/vendor-data/tag/[id]/page.tsx b/app/[lng]/partners/(partners)/vendor-data/tag/[id]/page.tsx new file mode 100644 index 00000000..7250732f --- /dev/null +++ b/app/[lng]/partners/(partners)/vendor-data/tag/[id]/page.tsx @@ -0,0 +1,43 @@ +import { Separator } from "@/components/ui/separator" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { TagsTable } from "@/lib/tags/table/tag-table" +import { searchParamsCache } from "@/lib/tags/validations" +import { getTags } from "@/lib/tags/service" + +interface IndexPageProps { + params: { + id: string + } + searchParams: Promise<SearchParams> +} + +export default async function TagPage(props: IndexPageProps) { + const resolvedParams = await props.params + const id = resolvedParams.id + + const idAsNumber = Number(id) + + // 2) SearchParams 파싱 (Zod) + // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 + const searchParams = await props.searchParams + const search = searchParamsCache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getTags({ + ...search, + filters: validFilters, + }, + idAsNumber) + ]) + + // 4) 렌더링 + return ( + <div className="space-y-6"> + <div> + <TagsTable promises={promises} selectedPackageId={idAsNumber}/> + </div> + </div> + ) +}
\ No newline at end of file diff --git a/app/[lng]/partners/page.tsx b/app/[lng]/partners/page.tsx new file mode 100644 index 00000000..245d0228 --- /dev/null +++ b/app/[lng]/partners/page.tsx @@ -0,0 +1,21 @@ +import { Metadata } from "next" +import { LoginForm } from "@/components/login/login-form" +import { Suspense } from "react" +import { LoginFormSkeleton } from "@/components/login/login-form-skeleton" + +export const metadata: Metadata = { + title: "Partner Portal", + description: "", +} + +export default function AuthenticationPage() { + + + return ( + <> + <Suspense fallback={<LoginFormSkeleton/>}> + <LoginForm /> + </Suspense> + </> + ) +} diff --git a/app/[lng]/partners/pq/page.tsx b/app/[lng]/partners/pq/page.tsx new file mode 100644 index 00000000..8ad23f6e --- /dev/null +++ b/app/[lng]/partners/pq/page.tsx @@ -0,0 +1,39 @@ +import { getServerSession } from "next-auth" +import { authOptions } from "@/app/api/auth/[...nextauth]/route" +import * as React from "react" +import { Shell } from "@/components/shell" +import { Skeleton } from "@/components/ui/skeleton" +import { getPQDataByVendorId } from "@/lib/pq/service" +import { PQInputTabs } from "@/components/pq/pq-input-tabs" + + +export default async function PQInputPage() { + // 세션 + const session = await getServerSession(authOptions) + // 예: 세션에서 vendorId 가져오기 + // const vendorId = session?.user.companyId + const vendorId = 17 // 임시 + const idAsNumber = Number(vendorId) + + // 1) 서버에서 PQ 데이터 조회 (groupName별로 묶인 구조) + const pqData = await getPQDataByVendorId(idAsNumber) + + return ( + <Shell className="gap-2"> + <div className="space-y-2"> + <h2 className="text-2xl font-bold tracking-tight"> + Pre-Qualification Check Sheet + </h2> + <p className="text-muted-foreground"> + PQ에 적절한 응답을 제출하시기 바랍니다. 진행 중 문의가 있으면 담당자에게 연락바랍니다. + </p> + </div> + + {/* 클라이언트 탭 UI 로드 (Suspense는 여기서는 크게 필요치 않을 수도 있음) */} + <React.Suspense fallback={<Skeleton className="h-7 w-52" />}> + <PQInputTabs data={pqData} vendorId={idAsNumber} /> + </React.Suspense> + + </Shell> + ) +}
\ No newline at end of file diff --git a/app/[lng]/partners/repository/page.tsx b/app/[lng]/partners/repository/page.tsx new file mode 100644 index 00000000..51c0fae5 --- /dev/null +++ b/app/[lng]/partners/repository/page.tsx @@ -0,0 +1,11 @@ +import { CompanyAuthForm } from "@/components/login/partner-auth-form" +import { Suspense } from "react" + +export default function RepositiryPage() { + return ( + <Suspense fallback={<div>Loading ...</div>}> + <CompanyAuthForm /> + </Suspense> + + ) +} diff --git a/app/[lng]/partners/signup/page.tsx b/app/[lng]/partners/signup/page.tsx new file mode 100644 index 00000000..26c2944b --- /dev/null +++ b/app/[lng]/partners/signup/page.tsx @@ -0,0 +1,21 @@ +import { Suspense } from "react" +import { Metadata } from "next" +import { JoinForm } from "@/components/signup/join-form" +import { JoinFormSkeleton } from "@/components/signup/join-form-skeleton" + +// (Optional) If Next.js attempts to statically optimize this page and you need full runtime +// behavior for query params, you may also need: +// export const dynamic = "force-dynamic" + +export const metadata: Metadata = { + title: "Partner Portal", + description: "Authentication forms built using the components.", +} + +export default function SignUpPage() { + return ( + <Suspense fallback={<JoinFormSkeleton/>}> + <JoinForm /> + </Suspense> + ) +}
\ No newline at end of file diff --git a/app/[lng]/qna/layout.tsx b/app/[lng]/qna/layout.tsx new file mode 100644 index 00000000..87651e92 --- /dev/null +++ b/app/[lng]/qna/layout.tsx @@ -0,0 +1,18 @@ +import { ReactNode } from 'react'; + + +export default function EvcpLayout({ children }: { children: ReactNode }) { + return ( + <div className="relative flex min-h-svh flex-col bg-background"> + <main className="flex flex-1 flex-col"> + <div className='container-wrapper'> + <div className="container flex-1 items-start md:grid md:grid-cols-[220px_minmax(0,1fr)] md:gap-6 lg:grid-cols-[240px_minmax(0,1fr)] lg:gap-10"> + {children} + </div> + + </div> + + </main> + </div> + ); +}
\ No newline at end of file diff --git a/app/[lng]/qna/page.tsx b/app/[lng]/qna/page.tsx new file mode 100644 index 00000000..10280464 --- /dev/null +++ b/app/[lng]/qna/page.tsx @@ -0,0 +1,8 @@ + +export default function Pages() { + return ( + <> + qna + </> + ) + }
\ No newline at end of file |
